package org.dreamwork.base.sh.dao.impl;

import org.dreamwork.base.sh.dao.IGenericDao;
import org.dreamwork.base.sh.Parameter;
import org.dreamwork.base.sh.Sort;
import org.dreamwork.base.sh.Operator;
import org.dreamwork.base.sh.finder.IFinderExecutor;
import org.dreamwork.base.sh.finder.IFinderNamingStrategy;
import org.dreamwork.base.sh.finder.IFinderArgumentTypeFactory;
import org.dreamwork.util.IDataCollection;
import org.dreamwork.util.ListDataCollection;
import org.springframework.orm.hibernate3.support.HibernateDaoSupport;
import org.hibernate.Session;
import org.hibernate.Query;
import org.hibernate.type.Type;

import java.io.Serializable;
import java.util.*;
import java.lang.reflect.Method;

/**
 * Created by IntelliJ IDEA.
 * User: seth
 * Date: 2009-8-21
 * Time: 14:20:14
 */
public class GenericHibernateDaoImpl<T, PK extends Serializable> extends HibernateDaoSupport implements IGenericDao<T, PK>, IFinderExecutor {

    protected Class<T> type;
    private IFinderArgumentTypeFactory argumentTypeFactory;
    private IFinderNamingStrategy namingStrategy;

    public GenericHibernateDaoImpl (Class<T> type) {
        this.type = type;
    }

    @SuppressWarnings ("unchecked")
    public PK save (T o) {
        return (PK) getSession ().save (o);
    }

    public void save (T... o) {
        save (Arrays.asList (o));
    }

    public void save (Collection<T> o) {
        Session session = getSession ();
        for (T t : o) session.save (t);
    }

    public void update (T o) {
        getSession ().update (o);
    }

    public void update (T... o) {
        update (Arrays.asList (o));
    }

    public void update (Collection<T> o) {
        Session session = getSession ();
        for (T t : o) session.update (t);
    }

    public void delete (T o) {
        getSession ().delete (o);
    }

    public void delete (T... o) {
        delete (Arrays.asList (o));
    }

    public void delete (Collection<T> o) {
        Session session = getSession ();
        for (T t : o) session.delete (t);
    }

    @SuppressWarnings ("unchecked")
    public T get (PK id) {
        return (T) getSession ().get (type, id);
    }

    @SuppressWarnings ("unchecked")
    public T get (Parameter... parameters) {
        Query query = buildQuery (null, parameters);
        query.setMaxResults (1);
        List list = query.list ();
        return list.size () > 0 ? (T) list.get (0) : null;
    }

    public T get (Collection<Parameter> parameters) {
        return get (toParameterArray (parameters));
    }

    @SuppressWarnings ("unchecked")
    public T get (Sort sort, Parameter... parameters) {
        Query query = buildQuery (sort, parameters);
        query.setMaxResults (1);
        List list = query.list ();
        return list.size () > 0 ? (T) list.get (0) : null;
    }

    public T get (Sort sort, Collection<Parameter> parameters) {
        return get (sort, toParameterArray (parameters));
    }

    public boolean exists (PK id) {
        return get (id) != null;
    }

    public boolean exists (Parameter... parameters) {
        return getCount (parameters) > 0;
    }

    public boolean exists (Collection<Parameter> parameters) {
        return exists (toParameterArray (parameters));
    }

    @SuppressWarnings ("unchecked")
    public List<T> find (Parameter... parameters) {
        Query query = this.buildQuery (null, parameters);
        return (List<T>) query.list ();
    }

    public List<T> find (Collection<Parameter> parameters) {
        return find (toParameterArray (parameters));
    }

    @SuppressWarnings ("unchecked")
    public IDataCollection<T> find (int pageNo, int pageSize, Sort sort, Parameter... parameters) {
        int count = getCount (parameters);
        Query query = buildQuery (pageSize, pageNo, sort, parameters);
        List<T> list = query.list ();

        ListDataCollection<T> ret = new ListDataCollection<T> ();
        ret.setData (list);
        ret.setPageNo (pageNo);
        ret.setPageSize (pageSize);
        ret.setTotalRows (count);

        return ret;
    }

    public IDataCollection<T> find (int pageNo, int pageSize, Sort sort, Collection<Parameter> parameters) {
        return find (pageNo, pageSize, sort, toParameterArray (parameters));
    }

    public int executeUpdate (String hql, Parameter... parameters) {
        Query query = getSession ().createQuery (hql);
        setQueryParameters (query, parameters);
        return query.executeUpdate ();
    }

    public int executeUpdate (String hql, Collection<Parameter> parameters) {
        return executeUpdate (hql, toParameterArray (parameters));
    }

    public List executeQuery (String hql, Parameter... parameters) {
        Query query = getSession ().createQuery (hql);
        for (Parameter p : parameters) {
            if (isArray (p.value)) query.setParameterList (p.name, toArray (p.value));
            else query.setParameter (p.name, p.value);
        }
        return query.list ();
    }

    public List executeQuery (String hql, Collection<Parameter> parameters) {
        return executeQuery (hql, toParameterArray (parameters));
    }

    @SuppressWarnings ("unchecked")
    public IDataCollection executeQuery (int pageNo, int pageSize, String hql, Parameter... parameters) {
        int p = hql.toLowerCase ().indexOf (" from ");
        String s = "SELECT COUNT(*) FROM " + hql.substring (p + " from ".length ());
        Query query = getSession ().createQuery (s);
        setQueryParameters (query, parameters);
        List list = query.list ();
        int count = ((Number) list.get (0)).intValue ();
        ListDataCollection c = new ListDataCollection ();
        c.setTotalRows (count);
        if (count == 0) return c;

        query = getSession ().createQuery (hql);
        setQueryParameters (query, parameters);
        query.setFirstResult ((pageNo - 1) * pageSize);
        query.setMaxResults (pageSize);
        c.setData (query.list ());
        return c;
    }

    public IDataCollection executeQuery (int pageNo, int pageSize, String hql, Collection<Parameter> parameters) {
        return executeQuery (pageNo, pageSize, hql, toParameterArray (parameters));
    }

    protected Query buildQuery (Sort sort, Parameter... parameters) {
        return buildQuery (-1, -1, sort, parameters);
    }

    protected Query buildQuery (int pageSize, int pageNo, Sort sort, Parameter... parameters) {
        Map<String, Object> map = new HashMap<String, Object> ();
        StringBuilder builder = new StringBuilder ();
        builder.append ("FROM ").append (type.getName ()).append (" AS t");
        if (parameters.length > 0) {
            builder.append (" WHERE ");
            builder.append (buildWhere ("t", map, parameters));
        }

        if (sort != null)
            builder.append (" ORDER BY ").append (sort.fieldName).append (" ").append (sort.direction);

        Query query = getSession ().createQuery (builder.toString ());
        setQueryParameters (query, map);

        if (pageSize != -1 && pageNo != -1) {
            int first = (pageNo - 1) * pageSize;
            query.setFirstResult (first);
            query.setMaxResults (pageSize);
        }
        return query;
    }

    protected int getCount (Parameter... parameters) {
        StringBuilder builder = new StringBuilder ();
        Map<String, Object> map = new HashMap<String, Object> ();
        builder.append ("SELECT count(*) FROM ").append (type.getName ()).append (" AS t");
        if (parameters.length > 0) {
            builder.append (" WHERE ").append (this.buildWhere ("t", map, parameters));
        }

        Query query = getSession ().createQuery (builder.toString ());
        setQueryParameters (query, map);

        List list = query.list ();
        return list.size () > 0 ? ((Number) list.get (0)).intValue () : 0;
    }

    protected StringBuilder buildWhere (String prefix, Map<String, Object> map, Parameter... parameters) {
        StringBuilder builder = new StringBuilder ();
        if (parameters.length != 0) {
            for (int i = 0; i < parameters.length; i++) {
                Parameter p = parameters[i];
                if (i > 0) builder.append (" AND ");
                if (prefix != null && prefix.trim ().length () != 0)
                    builder.append (prefix).append ('.');
                builder.append (p);
                if (p.operator == Operator.LIKE)
                    map.put (p.name, "%" + p.value + '%');
                else if (p.operator == Operator.BETWEEN) {
                    Object[] values = (Object[]) p.value;
                    map.put (p.name + "1", values[0]);
                    map.put (p.name + "2", values[1]);
                } else if (p.operator != Operator.IS_NOT_NULL && p.operator != Operator.IS_NULL)
                    map.put (p.name, p.value);
            }
        }
        return builder;
    }

    protected void setQueryParameters (Query query, Map<String, Object> map) {
        for (String name : map.keySet ()) {
            Object value = map.get (name);
            if (isArray (value)) {
                Object[] array = toArray (value);
                query.setParameterList (name, array);
            } else
                query.setParameter (name, map.get (name));
        }
    }

    protected void setQueryParameters (Query query, Parameter... parameters) {
        for (Parameter p : parameters) {
            if (isArray (p.value)) {
                Object[] array = toArray (p.value);
                query.setParameterList (p.name, array);
            } else
                query.setParameter (p.name, p.value);
        }
    }

    @SuppressWarnings ("unchecked")
    public List executeFinder (Method method, Object... queryArgs) {
        final Query namedQuery = prepareQuery (method, queryArgs);
        return (List<T>) namedQuery.list ();
    }

    @SuppressWarnings ("unchecked")
    public Iterator iterateFinder (Method method, Object... queryArgs) {
        final Query namedQuery = prepareQuery (method, queryArgs);
        return (Iterator<T>) namedQuery.iterate ();
    }

    private Query prepareQuery (Method method, Object... queryArgs) {
        final String queryName = getNamingStrategy ().queryNameFromMethod (type, method);
        final Query namedQuery = getSession ().getNamedQuery (queryName);
        String[] namedParameters = namedQuery.getNamedParameters ();
        if (namedParameters.length == 0) {
            setPositionalParams (namedQuery, queryArgs);
        } else {
            setNamedParams (namedQuery, namedParameters, queryArgs);
        }
        return namedQuery;
    }

    private void setPositionalParams (Query namedQuery, Object... queryArgs) {
        // Set parameter. Use custom Hibernate Type if necessary
        if (queryArgs != null) {
            for (int i = 0; i < queryArgs.length; i++) {
                Object arg = queryArgs[i];
                Type argType = getArgumentTypeFactory ().getArgumentType (arg);
                if (argType != null) {
                    namedQuery.setParameter (i, arg, argType);
                } else {
                    namedQuery.setParameter (i, arg);
                }
            }
        }
    }

    private void setNamedParams (Query namedQuery, String[] namedParameters, Object[] queryArgs) {
        // Set parameter. Use custom Hibernate Type if necessary
        if (queryArgs != null) {
            for (int i = 0; i < queryArgs.length; i++) {
                Object arg = queryArgs[i];
                Type argType = getArgumentTypeFactory ().getArgumentType (arg);
                if (argType != null) {
                    namedQuery.setParameter (namedParameters[i], arg, argType);
                } else {
                    if (arg instanceof Collection) {
                        namedQuery.setParameterList (namedParameters[i], (Collection) arg);
                    } else {
                        namedQuery.setParameter (namedParameters[i], arg);
                    }
                }
            }
        }
    }

    public IFinderNamingStrategy getNamingStrategy () {
        return namingStrategy;
    }

    public void setNamingStrategy (IFinderNamingStrategy namingStrategy) {
        this.namingStrategy = namingStrategy;
    }

    public IFinderArgumentTypeFactory getArgumentTypeFactory () {
        return argumentTypeFactory;
    }

    public void setArgumentTypeFactory (IFinderArgumentTypeFactory argumentTypeFactory) {
        this.argumentTypeFactory = argumentTypeFactory;
    }

    private boolean isArray (Object o) {
        if (o == null) return false;
        String className = o.getClass ().getName ();
        return className.indexOf ('[') != -1;
    }

    private Object[] toArray (Object o) {
        if (!isArray (o)) return new Object[] {o};
        if (o instanceof long[]) {
            long[] arr = (long[]) o;
            Long[] ret = new Long [arr.length];
            for (int i = 0; i < arr.length; i ++) ret [i] = arr [i];
            return ret;
        }
        if (o instanceof int[]) {
            int[] arr = (int[]) o;
            Integer[] ret = new Integer [arr.length];
            for (int i = 0; i < arr.length; i ++) ret [i] = arr [i];
            return ret;
        }
        if (o instanceof byte[]) {
            byte[] arr = (byte[]) o;
            Byte[] ret = new Byte [arr.length];
            for (int i = 0; i < arr.length; i ++) ret [i] = arr [i];
            return ret;
        }
        if (o instanceof short[]) {
            short[] arr = (short[]) o;
            Short[] ret = new Short [arr.length];
            for (int i = 0; i < arr.length; i ++) ret [i] = arr [i];
            return ret;
        }
        if (o instanceof char[]) {
            char[] arr = (char[]) o;
            Character[] ret = new Character [arr.length];
            for (int i = 0; i < arr.length; i ++) ret [i] = arr [i];
            return ret;
        }
        if (o instanceof boolean[]) {
            boolean[] arr = (boolean[]) o;
            Boolean[] ret = new Boolean [arr.length];
            for (int i = 0; i < arr.length; i ++) ret [i] = arr [i];
            return ret;
        }
        if (o instanceof float[]) {
            float[] arr = (float[]) o;
            Float[] ret = new Float [arr.length];
            for (int i = 0; i < arr.length; i ++) ret [i] = arr [i];
            return ret;
        }
        if (o instanceof double[]) {
            double[] arr = (double[]) o;
            Double[] ret = new Double [arr.length];
            for (int i = 0; i < arr.length; i ++) ret [i] = arr [i];
            return ret;
        }
        return (Object[]) o;
    }

    private Parameter[] toParameterArray (Collection<Parameter> coll) {
        Parameter[] array = new Parameter[coll.size ()];
        return coll.toArray (array);
    }
}